from legocalendar import *
from legocalendar.google_calendar_export import *
from legocalendar.models import *
from legocalendar.rules import DrinkingRule, EatingRule1, EatingRule2, \
    ExerciseRule, SleepRule1, SleepRule2, SocializeRule, SpiritualRule, StudyRule, \
    WorkRule
from pyramid.response import Response, FileResponse
from pyramid.view import view_config
from xlutils.copy import copy
from xlwt import Style, XFStyle, Borders, Pattern, easyxf
import datetime
import json
import mimetypes
import os
import pickle
import time
import xlrd
import xlutils
import xlwt


### HELPER FUNCTIONS ###

#*** Excel Export Stuff ***

#*** Maps an Activity to a Style-Color
activity_style = {
    'eating': easyxf('pattern: pattern solid_fill, fore_color red;'
                     'font: color_index white;'),
    'spiritual': easyxf('pattern: pattern solid_fill, fore_color orange;'),
    'class': easyxf('pattern: pattern solid_fill, fore_color yellow;'),
    'studying': easyxf('pattern: pattern solid_fill, fore_color green;'
                       'font: color_index white;'),
    'work' : easyxf('pattern: pattern solid_fill, fore_color light_green;'),
    'exercising':easyxf('pattern: pattern solid_fill, fore_color blue;'
                        'font: colour_index white;'),
    'socializing':easyxf('pattern: pattern solid_fill, fore_color gray25;'),
    'sleeping': easyxf('pattern: pattern solid_fill, fore_color black;'
                       'font: color_index white;'),
    'drinking': easyxf('pattern: pattern solid_fill, fore_color brown;'
                       'font: color_index white;')
}

# fixes the numbers in timeblocks
def fixNumbers (timeblocks):
    return map( lambda tb: { 'day'      : tb['day'],
                             'alignment': tb['alignment'],
                             'hour'     : tb['hour']/1.0,
                             'duration' : tb['duration']/60.0,
                             'activity' : tb['activity'] },
                timeblocks)


# Timeblock Consolidation: Merges adjecent timeblocks (both vertically and horizontally) and returns a consolidated list of timeblocks
def consolidate (timeblocks):
    condTimeblocks = []

     # For the User's sake, we need to condense activities that are adjacent and identical.
    for i in range(7):

        singleDay = sorted( filter( lambda tb : tb['day'] == i, timeblocks ), 
                            key=lambda tb: tb['hour'])
        for j in range(3):
            blocks = filter(lambda tb: tb['alignment'] == j, singleDay)

            # If two blocks reside in the same cell, their start-times are equal. We need to fix that:
            k = 1;
            for tb in blocks:
                if k < len(blocks) and (tb['hour'] == blocks[k]['hour']): # if the next timeblock is in the same cell:
                    blocks[k]['hour'] += tb['duration']                   # move its start-time up by tb['duration']
                k += 1
                    
            # traverse that day and condense adjacent & identical activities
            k = 1
            for tb in blocks:
                while (k < len(blocks) and 
                       (tb['activity'] == blocks[k]['activity'] and
                        (tb['hour']+tb['duration']) == blocks[k]['hour'])):
                    tb['duration'] += blocks.pop(k)['duration']
                k += 1
                condTimeblocks.append(tb)

    # Now we need to condense blocks that are side-by-side
    newTimeblocks = []
    for i in range(7):
        singleDay = sorted( filter( lambda tb : tb['day'] == i, condTimeblocks ),
                            key=lambda tb: tb['hour'] )

        for tb in singleDay:
            matches = filter(lambda b: 
                             ( b != tb and
                               b['activity'] == tb['activity'] and
                               b['hour'] >= tb['hour'] and
                               b['hour'] <= tb['hour'] + tb['duration'] ),
                             singleDay)
            for m in matches:
                tbEnd = tb['hour'] + tb['duration']
                mEnd = m['hour'] + m['duration']
                if tbEnd < mEnd: tb['duration'] += (mEnd - tbEnd)
                singleDay.remove(m)
            
            newTimeblocks.append(tb)

    return newTimeblocks


def get_mimetype(filename):
    type, encoding = mimetypes.guess_type(filename)
    # We'll ignore encoding, even though we shouldn't really
    return type or 'application/vnd.ms-excel'

def analyze(timeblocks):
    return {
        'drinking'    : DrinkingRule(timeblocks),
        'breakfast'   : EatingRule1(timeblocks),
        'threeMeals'  : EatingRule2(timeblocks),
        'exercise'    : ExerciseRule(timeblocks),
        'sevenHours'  : SleepRule1(timeblocks),
        'yoyoSleep'   : SleepRule2(timeblocks),
        'socializing' : SocializeRule(timeblocks),
        'spiritual'   : SpiritualRule(timeblocks),
        'study'       : StudyRule(timeblocks),
        'work'        : WorkRule(timeblocks)
        }


# Saves various Datas and the analysis results in the database
def save_calendar(rules, timeblocks, isReal):
    session = make_legocal_session()
 
    a = analysis.Analysis(rules, isreal = isReal)
    session.add(a)
    session.flush()

    session.add(datas.Datas(timeblocks, uid = a.uid, isreal = isReal))
    session.flush()
    
    session.commit()
    session.close()


    
### STANDARD VIEWS ###

# The frontpage view that our user sees. 
# It returns the index.pt template, with an empty JSON object (denoted by the 'return {}')
@view_config(route_name='main_view', renderer='templates/index.pt')
def main_view (request):
    return {}



# Saves the calendar to the database and returns an analysis
@view_config(route_name='analyze_calendar', renderer='json')
def analyze_calendar (request):
    timeblocks = consolidate(consolidate(consolidate(fixNumbers(request.json_body['timeblocks'])))) # 3 times because of 3 columns per day
    isReal = request.GET['isReal']

    # Build Analysis:
    analysis = "<h2>Schedule Analysis:</h2>"
    rules = analyze(timeblocks)
    for key in rules:
        analysis += rules[key].getMessage() + "\n"

    if isReal == 'yes':
        save_calendar(rules, timeblocks, True)

    return {'analysis': analysis}


# When user clicks Export to Google Button:
@view_config(route_name='export_calendar', renderer='json')
def export_calendar (request):
    timeblocks = fixNumbers(request.json_body['timeblocks'])
    condTimeblocks = consolidate(consolidate(consolidate(timeblocks)))

    # save ideal calendar
    save_calendar(analyze(condTimeblocks), condTimeblocks, False)

    # We need to format the timeblocks for Google Event strings:
    # Backtrack to the timestamp of the latest Sunday closest to Now
    currTime = time.time()
    d = datetime.datetime.fromtimestamp(currTime)
    backtrack=0
    while (d.weekday() != 6):
        backtrack += (60*60*24)
        d = datetime.datetime.fromtimestamp(currTime-backtrack) # decrement d by another day

    # Construct events out of the timeblocks
    calEvents = []
    for tb in condTimeblocks:
        startHour = str(int( tb['hour'] ) )
        startMins = str(int((tb['hour']%1)*60 ) )
        endHour   = str(int( tb['hour']+tb['duration'] ) )
        endMins   = str(int(((tb['hour']+tb['duration'])%1)*60 ) )

        # Determine the day that is tb['day'] days ahead of where d is
        d_next = d + datetime.timedelta(days=tb['day'])
        year = str( d_next.year )
        month = str( d_next.month )
        day =  str( d_next.day )
        

        # if endHour == 00:00 (midnight), set it to 23:59, otherwise Google vomits everywhere
        if endHour == '24' and endMins == '0':
            endHour = '23'
            endMins = '59'
       
        calEvents.append({
                    'summary' : tb['activity'],
                    'startTime':
                        year + '-' + month + '-' + day +\
                        'T' + startHour + ':' + startMins + ':00.000',
                    'endTime':
                        year + '-' + month + '-' + day +\
                        'T' + endHour + ':' + endMins +':00.000'
                    })

    currTimeMillis = int(time.time()*1000)
    tempFile = str(currTimeMillis) + '.pkl'
    output = open('./legocalendar/tmp/'+tempFile, 'wb')
    pickle.dump(calEvents, output)
    output.close()
    return tempFile


# Google Oauth2 Callback:
@view_config(route_name='oauth2callback', renderer='templates/oauth2callback.pt')
def oauth_callback (request):
    return {}


# Get Calendar Events from file specified in "state" parameter
# Export everything to Google Calendar:
@view_config(route_name='process_google_auth', renderer='string')
def process_google_auth (request):
    params = request.json_body

    # Retrieve the user's calEvents (specified by a filename in the url) and then delete the file
    filepath = './legocalendar/tmp/'
    pkl_file = open(filepath+params['state'], 'rb')
    calEvents = pickle.load(pkl_file)
    pkl_file.close()
    os.remove(filepath+params['state'])

    google_calendar_export(calEvents, params['access_token'])



# When User clicks Download in Excel
@view_config(route_name='excel_calendar', renderer='string')
def excel_calendar(request):
    timeblocks = json.loads(request.POST['data'])['timeblocks']
    filename   = request.POST['filename']
    condTimeblocks = consolidate(consolidate(consolidate(fixNumbers(timeblocks))))

    save_calendar(analyze(condTimeblocks), condTimeblocks, False)

    for tb in condTimeblocks: tb['col'] = None

    # Open up template.xls, which has the basic format of an empty calendar
    rbk = xlrd.open_workbook('./legocalendar/template.xls', formatting_info=True)
    wbk = copy(rbk)
    wsh = wbk.get_sheet(0)

    # set offset of cells (due to headers)
    offset = 1

    for i in range(7):
        singleDay = sorted(filter(lambda tb: tb['day'] == i, condTimeblocks),
                           key = lambda tb: tb['duration'],
                           reverse = True)

        for tb in singleDay:
            style = activity_style[tb['activity']]
            row = (offset + (tb['hour']*4))
            tb['col'] = (tb['day']*3) + offset   
            
            # find blocks who start within this blocks timeframe
            overlaps = filter(lambda b: 
                              (b != tb and b['col'] != None) and 
                              ((b['hour'] >= tb['hour'] and b['hour'] < tb['hour'] + tb['duration'])
                               or
                               (b['hour'] <= tb['hour'] and b['hour'] + b['duration'] > tb['hour'])),
                              singleDay)
            
            # if there exists within overlaps a block within the same column then increment col
            if (filter(lambda b: b['col'] == tb['col'], overlaps)): tb['col'] += 1
            if (filter(lambda b: b['col'] == tb['col'], overlaps)): tb['col'] += 1
            
            # write to file
            wsh.write(row, tb['col'], tb['activity'], style)
        
            durationCells = (tb['duration'] * 4) - 1
            for x in range(int(durationCells)):
                wsh.write(row+x+1, tb['col'], '', style)

    # Write Contents to file:
    filepath = './legocalendar/tmp/'+filename
    wbk.save(filepath)    

    # now serve that file to the User
    f = open(filepath, 'r').read()
    res = Response(content_type = get_mimetype(filepath))
    res.body = f
    res.content_disposition = 'attachment;filename="healthy_calendar.xls"'
    os.remove(filepath)
    return res



# Analyzes the aggregate data and serve an Excel spreadsheet containing the analysis
@view_config(route_name='analysis', renderer='string')
def get_analysis(request):
    session = make_legocal_session()
    results = session.query(analysis.Analysis).all()

    # Analyze all participant data:
    numParticipants = len(results)
    percent_rules_broke = {
        'drinking'    : sum(map(lambda row: row.drinking_rule, results))    / float(numParticipants) * 100,
        'breakfast'   : sum(map(lambda row: row.breakfast_rule, results))   / float(numParticipants) * 100,
        'threeMeals'  : sum(map(lambda row: row.threeMeals_rule, results))  / float(numParticipants) * 100,
        'exercise'    : sum(map(lambda row: row.exercise_rule, results))    / float(numParticipants) * 100,
        'sevenHours'  : sum(map(lambda row: row.sevenHours_rule, results))  / float(numParticipants) * 100,
        'yoyoSleep'   : sum(map(lambda row: row.yoyoSleep_rule, results))   / float(numParticipants) * 100,
        'socializing' : sum(map(lambda row: row.socializing_rule, results)) / float(numParticipants) * 100,
        'spiritual'   : sum(map(lambda row: row.spiritual_rule, results))   / float(numParticipants) * 100,
        'study'       : sum(map(lambda row: row.study_rule, results))       / float(numParticipants) * 100,
        'work'        : sum(map(lambda row: row.work_rule, results))        / float(numParticipants) * 100
         }

    session.close()


    # Compile an Excel Spreadsheet
    wbk   = xlwt.Workbook()
    sheet = wbk.add_sheet('sheet1')

    # Setup a bold font for headers
    style = xlwt.XFStyle()
    font = xlwt.Font()
    font.name = 'Times New Roman'
    font.bold = True
    style.font = font

    # Insert all the text:
    d = datetime.datetime.fromtimestamp(time.time())
    sheet.write(0,0, 'Generated data analysis:', style)
    sheet.write(2,0, 'Date:')
    sheet.write(2,1, '%s / %s / %s - %s:%s' % (d.year, d.month, d.day, d.hour, d.minute))
    sheet.write(3,0, 'Participants:')
    sheet.write(3,1, numParticipants)
    sheet.write(4,0, 'Students')
    sheet.write(4,1, 'X')
    sheet.write(6,1, '% participants who broke rule')

    row = 8
    for name, val in percent_rules_broke.iteritems():
        sheet.write(row,0, name)
        sheet.write(row,1, val)
        row += 1

    # Save File to disk
    filepath = './legocalendar/static/analysis/analysis.xls'
    wbk.save(filepath)

    # now serve that file to the User
    f = open(filepath, 'r').read()
    res = Response(content_type = get_mimetype(filepath))
    res.body = f
    res.content_disposition = 'attachment;filename="analysis.xls"'
    return res
